因為從 DX 開始寫引擎這基本上跟從零寫底層數學差不多,其中 Vector、Matrix 都需要自己撰寫相關的定義與演算。
尤其做的過程中,深刻地感受到 Shader 的計算真的是一個關係到另一個,光是 row_major 跟 column_major 沒設定好就會造成後面計算的混亂。
在這個大半時間用 AI 寫 Code 的時代,如果有地方需要重構的時候,當然現在 AI 已經會自己確認計算結果是否正確,不過有個可視化的範例還是比較好點的。
當然測試有很多層級,從最底層測試單一 function 的 Unit Test,到上層確定組合後的結果的 Integration Test。不過這邊就先以較簡單的部分來展示一下就好。
這邊要裝的是 Catch2 ,先在 powershell 安裝
cd vcpkg
vcpkg add port catch2
之後在專案根目錄的 CMakeLists.txt 輸入以下內容
include(CTest)
if(BUILD_TESTING)
find_package(Catch2 3 CONFIG REQUIRED)
target_link_libraries(SkylineTests PRIVATE Catch2::Catch2WithMain)
include(Catch)
catch_discover_tests(SkylineTests)
endif()
之後底下像是我測試 math 的設定會像這樣
add_executable(SkylineMathTests
vector2_test.cpp
vector3_test.cpp
vector4_test.cpp
matrix_test.cpp
../../math/vector.cpp
)
target_compile_features(SkylineMathTests PRIVATE cxx_std_20)
target_include_directories(SkylineMathTests PRIVATE ../..)
target_link_libraries(SkylineMathTests PRIVATE
Catch2::Catch2WithMain
Microsoft::DirectXMath
)
catch_discover_tests(SkylineMathTests)
這裡舉其中測試 Vector2 的程式為範例
#include "vector_test_helpers.h"
TEST_CASE("Vector2 construction and assignment preserve components", "[math][vector2]")
{
CheckVector(Vector2{}, {0, 0});
CheckVector(Vector2::Zero, {0, 0});
Vector2 value{2, -3};
CheckVector(value, {2, -3});
value.Set(-4, 5);
Vector2 copy;
copy = value;
value.Set(0, 0);
CheckVector(copy, {-4, 5});
}
TEST_CASE("Vector2 interpolation reaches endpoints and midpoint", "[math][vector2]")
{
const Vector2 start{-2, 4};
const Vector2 end{6, -8};
Vector2 value;
value.Lerp(0, start, end);
CheckVector(value, start);
value.Lerp(0.5f, start, end);
CheckVector(value, {2, -2});
value.Lerp(1, start, end);
CheckVector(value, end);
}
TEST_CASE("Vector2 normalization preserves direction", "[math][vector2]")
{
Vector2 value{3, -4};
value.Normalize();
CheckVector(value, {0.6f, -0.8f});
value.Normalize();
CheckVector(value, {0.6f, -0.8f});
}
之後開 Visual Studio 的 Test Explorer 就能看到結果

另外就是也能看 HLSL 實際輸出的數字對不對,不過這邊的程式還有些沒整理好先參考就好
#include <catch2/catch_test_macros.hpp>
#include <Windows.h>
#include <d3dcompiler.h>
#include <directx/dxgiformat.h>
#include <directx/d3d12.h>
#include <directx/d3dx12.h>
#include <dxgi1_6.h>
#include <wrl/client.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include "shader.h"
namespace
{
using Microsoft::WRL::ComPtr;
constexpr UINT targetWidth = 8;
constexpr UINT targetHeight = 8;
constexpr DXGI_FORMAT targetFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
struct Vertex
{
float x;
float y;
float z;
};
using Pixel = std::array<std::uint8_t, 4>;
struct RenderedPixels
{
Pixel center;
Pixel corner;
};
void checkHr(HRESULT result, const char* message)
{
if (FAILED(result))
throw std::runtime_error(message);
}
D3D12_HEAP_PROPERTIES heapProperties(D3D12_HEAP_TYPE type)
{
D3D12_HEAP_PROPERTIES properties{};
properties.Type = type;
properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
properties.CreationNodeMask = 1;
properties.VisibleNodeMask = 1;
return properties;
}
D3D12_RESOURCE_DESC bufferDescription(UINT64 size)
{
D3D12_RESOURCE_DESC description{};
description.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
description.Width = size;
description.Height = 1;
description.DepthOrArraySize = 1;
description.MipLevels = 1;
description.SampleDesc.Count = 1;
description.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
return description;
}
ComPtr<ID3D12RootSignature> createRootSignature(ID3D12Device* device)
{
D3D12_ROOT_SIGNATURE_DESC description{};
description.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
ComPtr<ID3DBlob> serialized;
ComPtr<ID3DBlob> errors;
checkHr(D3D12SerializeRootSignature(&description, D3D_ROOT_SIGNATURE_VERSION_1,
&serialized, &errors),
"Triangle shader test: Failed to serialize root signature.");
ComPtr<ID3D12RootSignature> rootSignature;
checkHr(device->CreateRootSignature(0, serialized->GetBufferPointer(),
serialized->GetBufferSize(),
IID_PPV_ARGS(&rootSignature)),
"Triangle shader test: Failed to create root signature.");
return rootSignature;
}
ComPtr<ID3D12PipelineState> createPipeline(ID3D12Device* device,
ID3D12RootSignature* rootSignature)
{
Shader vertexShader;
Shader pixelShader;
vertexShader.loadVS(TRIANGLE_SHADER_PATH, "VSMain");
pixelShader.loadPS(TRIANGLE_SHADER_PATH, "PSMain");
constexpr D3D12_INPUT_ELEMENT_DESC inputElements[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
};
D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};
description.InputLayout = {inputElements, 1};
description.pRootSignature = rootSignature;
description.VS = {vertexShader.getCompiledBlob()->GetBufferPointer(),
vertexShader.getCompiledBlob()->GetBufferSize()};
description.PS = {pixelShader.getCompiledBlob()->GetBufferPointer(),
pixelShader.getCompiledBlob()->GetBufferSize()};
description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
description.DepthStencilState.DepthEnable = FALSE;
description.DepthStencilState.StencilEnable = FALSE;
description.SampleMask = UINT_MAX;
description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
description.NumRenderTargets = 1;
description.RTVFormats[0] = targetFormat;
description.SampleDesc.Count = 1;
ComPtr<ID3D12PipelineState> pipeline;
checkHr(device->CreateGraphicsPipelineState(&description, IID_PPV_ARGS(&pipeline)),
"Triangle shader test: Failed to create graphics pipeline.");
return pipeline;
}
RenderedPixels renderTriangle()
{
ComPtr<ID3D12Device> device;
checkHr(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device)),
"Triangle shader test: Failed to create D3D12 device.");
D3D12_COMMAND_QUEUE_DESC queueDescription{};
queueDescription.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ComPtr<ID3D12CommandQueue> queue;
checkHr(device->CreateCommandQueue(&queueDescription, IID_PPV_ARGS(&queue)),
"Triangle shader test: Failed to create command queue.");
ComPtr<ID3D12CommandAllocator> allocator;
checkHr(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&allocator)),
"Triangle shader test: Failed to create command allocator.");
auto rootSignature = createRootSignature(device.Get());
auto pipeline = createPipeline(device.Get(), rootSignature.Get());
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDescription{};
rtvHeapDescription.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDescription.NumDescriptors = 1;
ComPtr<ID3D12DescriptorHeap> rtvHeap;
checkHr(device->CreateDescriptorHeap(&rtvHeapDescription, IID_PPV_ARGS(&rtvHeap)),
"Triangle shader test: Failed to create RTV heap.");
D3D12_RESOURCE_DESC targetDescription{};
targetDescription.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
targetDescription.Width = targetWidth;
targetDescription.Height = targetHeight;
targetDescription.DepthOrArraySize = 1;
targetDescription.MipLevels = 1;
targetDescription.Format = targetFormat;
targetDescription.SampleDesc.Count = 1;
targetDescription.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
targetDescription.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
D3D12_CLEAR_VALUE clearValue{};
clearValue.Format = targetFormat;
const auto defaultHeap = heapProperties(D3D12_HEAP_TYPE_DEFAULT);
ComPtr<ID3D12Resource> target;
checkHr(device->CreateCommittedResource(&defaultHeap, D3D12_HEAP_FLAG_NONE,
&targetDescription, D3D12_RESOURCE_STATE_RENDER_TARGET,
&clearValue, IID_PPV_ARGS(&target)),
"Triangle shader test: Failed to create render target.");
const auto rtv = rtvHeap->GetCPUDescriptorHandleForHeapStart();
device->CreateRenderTargetView(target.Get(), nullptr, rtv);
constexpr Vertex vertices[] = {
{-0.5f, -0.5f, 0.0f},
{0.0f, 0.5f, 0.0f},
{0.5f, -0.5f, 0.0f},
};
const auto uploadHeap = heapProperties(D3D12_HEAP_TYPE_UPLOAD);
const auto vertexDescription = bufferDescription(sizeof(vertices));
ComPtr<ID3D12Resource> vertexBuffer;
checkHr(device->CreateCommittedResource(&uploadHeap, D3D12_HEAP_FLAG_NONE,
&vertexDescription, D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr, IID_PPV_ARGS(&vertexBuffer)),
"Triangle shader test: Failed to create vertex buffer.");
void* vertexData = nullptr;
D3D12_RANGE noRead{0, 0};
checkHr(vertexBuffer->Map(0, &noRead, &vertexData),
"Triangle shader test: Failed to map vertex buffer.");
std::memcpy(vertexData, vertices, sizeof(vertices));
vertexBuffer->Unmap(0, nullptr);
D3D12_VERTEX_BUFFER_VIEW vertexView{};
vertexView.BufferLocation = vertexBuffer->GetGPUVirtualAddress();
vertexView.SizeInBytes = sizeof(vertices);
vertexView.StrideInBytes = sizeof(Vertex);
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{};
UINT64 readbackSize = 0;
device->GetCopyableFootprints(&targetDescription, 0, 1, 0, &footprint,
nullptr, nullptr, &readbackSize);
const auto readbackHeap = heapProperties(D3D12_HEAP_TYPE_READBACK);
const auto readbackDescription = bufferDescription(readbackSize);
ComPtr<ID3D12Resource> readback;
checkHr(device->CreateCommittedResource(&readbackHeap, D3D12_HEAP_FLAG_NONE,
&readbackDescription, D3D12_RESOURCE_STATE_COPY_DEST,
nullptr, IID_PPV_ARGS(&readback)),
"Triangle shader test: Failed to create readback buffer.");
ComPtr<ID3D12GraphicsCommandList> commands;
checkHr(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(),
pipeline.Get(), IID_PPV_ARGS(&commands)),
"Triangle shader test: Failed to create command list.");
const D3D12_VIEWPORT viewport{0.0f, 0.0f, float(targetWidth), float(targetHeight), 0.0f, 1.0f};
const D3D12_RECT scissor{0, 0, LONG(targetWidth), LONG(targetHeight)};
constexpr float black[] = {0.0f, 0.0f, 0.0f, 1.0f};
commands->RSSetViewports(1, &viewport);
commands->RSSetScissorRects(1, &scissor);
commands->OMSetRenderTargets(1, &rtv, FALSE, nullptr);
commands->ClearRenderTargetView(rtv, black, 0, nullptr);
commands->SetGraphicsRootSignature(rootSignature.Get());
commands->SetPipelineState(pipeline.Get());
commands->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commands->IASetVertexBuffers(0, 1, &vertexView);
commands->DrawInstanced(3, 1, 0, 0);
D3D12_RESOURCE_BARRIER barrier{};
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barrier.Transition.pResource = target.Get();
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE;
commands->ResourceBarrier(1, &barrier);
D3D12_TEXTURE_COPY_LOCATION destination{};
destination.pResource = readback.Get();
destination.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
destination.PlacedFootprint = footprint;
D3D12_TEXTURE_COPY_LOCATION source{};
source.pResource = target.Get();
source.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
source.SubresourceIndex = 0;
commands->CopyTextureRegion(&destination, 0, 0, 0, &source, nullptr);
checkHr(commands->Close(), "Triangle shader test: Failed to close command list.");
ID3D12CommandList* commandLists[] = {commands.Get()};
queue->ExecuteCommandLists(1, commandLists);
ComPtr<ID3D12Fence> fence;
checkHr(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)),
"Triangle shader test: Failed to create fence.");
checkHr(queue->Signal(fence.Get(), 1), "Triangle shader test: Failed to signal fence.");
std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)> event(
CreateEventW(nullptr, FALSE, FALSE, nullptr), &CloseHandle);
if (!event)
throw std::runtime_error("Triangle shader test: Failed to create fence event.");
checkHr(fence->SetEventOnCompletion(1, event.get()),
"Triangle shader test: Failed to wait for GPU completion.");
if (WaitForSingleObject(event.get(), INFINITE) != WAIT_OBJECT_0)
throw std::runtime_error("Triangle shader test: GPU wait failed.");
D3D12_RANGE readRange{0, static_cast<SIZE_T>(readbackSize)};
void* mapped = nullptr;
checkHr(readback->Map(0, &readRange, &mapped),
"Triangle shader test: Failed to map readback buffer.");
const auto* bytes = static_cast<const std::uint8_t*>(mapped) + footprint.Offset;
const auto pixelAt = bytes, &footprint {
Pixel pixel{};
std::memcpy(pixel.data(), bytes + y * footprint.Footprint.RowPitch + x * pixel.size(),
pixel.size());
return pixel;
};
RenderedPixels result{pixelAt(4, 4), pixelAt(0, 0)};
D3D12_RANGE noWrite{0, 0};
readback->Unmap(0, &noWrite);
return result;
}
}
TEST_CASE("triangle shader renders an opaque red triangle", "[gpu][triangle]")
{
const auto pixels = renderTriangle();
REQUIRE(pixels.center == Pixel{255, 0, 0, 255});
REQUIRE(pixels.corner == Pixel{0, 0, 0, 255});
}
https://github.com/catchorg/catch2